#!/usr/bin/env python

#   Trolls Outta Luckland v1.1.0
#   Copyright 2009-2010, Gummbum
#
#   This file is part of Trolls Outta Luckland.
#
#   Trolls Outta Luckland is free software: you can redistribute it
#   and/or modify it under the terms of the GNU General Public License
#   as published by the Free Software Foundation, either version 3 of
#   the License, or (at your option) any later version.
#
#   Trolls Outta Luckland is distributed in the hope that it will be
#   useful, but WITHOUT ANY WARRANTY; without even the implied warranty
#   of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with Trolls Outta Luckland.  If not, see
#   <http://www.gnu.org/licenses/>.

"""
"""

__version__ = '1.1.0'

### Python imports
from math import cos, radians, sin
import os
from os.path import join as join_path
import sys
import random
from random import randrange

try:
    ### Pygame imports
    import pygame
    from pygame.locals import Color, RLEACCEL, K_0
    ### Game library imports
    import game_globals
    from game_globals import *
    import gumm_sound
    import player
    from pixelperfect import get_colorkey_hitmask
    from powerups import ExtraShipDebris
    from sprite_strip_anim import SpriteStripAnim
    import spritesheet
    from ui import Herald
    from utils import (
        calc_angle, calc_line, calc_circumference_point, constrain, distance,
        draw_text, get_font, load_image, plot_curve
    )
    from vec2d import *
except ImportError, e:
    print 'ImportError: %s' % e
    print '    in file "%s"' % os.path.abspath(sys.argv[0])

# _SCORES must equal the size of _STATES
_STATES = (CONVOY,
 GOTO_STANDBY,
 STANDBY,
 PATROL,
 PAUSE,
 END_PATROL,
 REFORM,
 DERELICT,
 RAM) = range(9)
_SCORES = (
    50,     # CONVOY
    1,      # GOTO_STANDBY
    50,     # STANDBY
    250,    # PATROL
    250,    # PAUSE,
    250,    # END_PATROL,
    1,      # REFORM
    50,     # DERELICT
    500,    # RAM
)

# Globals initialized by Factory constructor
game = None
window = None
game_rect = None
jukebox = None

# Ship angle size of 1 makes turns *very* smooth at the significant cost of
# initial compile time and memory. If you change this you'll need to edit
# bin/make-hitmasks.py and run it to regen the hitmasks module code.
_SPACE1_SPRITE_SHEET = join_path('image', 'SPACE1x150.png')
_SPACE2_SPRITE_SHEET = join_path('image', 'SPACE2x150.png')
_KAMIKAZE_SPRITE_SHEET = join_path('image', 'Kamikaze.png')
_SHIP_ANGLE_SIZE = 8

def iround(n):
    return int(round(n))

def w(*args):
    s = ' '.join(args)
    sys.stdout.write(s)
    sys.stdout.flush()

class RingBuffer:
    def __init__(self, size):
        self.data = [None for i in xrange(size)]
    def append(self, *args):
        for arg in args:
            self._append(arg)
    def get(self):
        return self.data
    def _append(self, x):
        self.data.pop(0)
        self.data.append(x)

class Missile(pygame.sprite.Sprite):
    def __init__(self, ship, angle=None):
        pygame.sprite.Sprite.__init__(self)
        self.ship = ship
        variance = randrange(0,20) / 100.0 - 0.1
        self.speed = 0.9 * game.speed()
        self.speed_carry = 0.0
        if angle is None:
            self.angle = ship.angle % 360
        else:
            self.angle = angle % 360
        if self.angle == 0:
            self.angle = 360
        self.angle_size = 8
        self.turn_speed = 0.35 * game.speed()
        self.anim = SpriteStripAnim(join_path('image', 'KABOOMx125.png'),
            (211,48,20,20), 1, -1, False,
            hitmasks=True, angles=range(0,361,self.angle_size),
            name='enemy_missile')
        self.anim.iter()
        self.rect = pygame.Rect(ship.rect)
        self.centerf = vec2d(self.rect.center)
        self._set_image()
        game.groups.add(self, layer='enemy_bots')
    def hit(self, others=()):
        self.kill()
        game.groups.add(MissileExplodes(
            self.rect.center), layer='enemies', under=True)
        if len(others) == 0 and game.player.alive():
            # Bonus for dodging a live missile until end of the wave
            game.score.add(_SCORES[PATROL], 5, self.rect.center)
        else:
            # Normal score for shooting a missile
            for other in others:
                if issubclass(type(other), player.LaserBlast):
                    game.score.add(_SCORES[PATROL], 1, self.rect.center)
                    other.kill()
    def update(self, *args):
        self._turn_towards(game.player.rect.center)
        self.centerf.x += -sin(radians(self.angle)) * self.speed
        self.centerf.y += -cos(radians(self.angle)) * self.speed
        self.rect.centerx = iround(self.centerf.x)
        self.rect.centery = iround(self.centerf.y)
    def _round_angle(self):
        return iround(self.angle / self.angle_size) * self.angle_size
    def _set_image(self):
        self.image,self.hitmask = self.anim.get_angle(0, self._round_angle())
        pos = self.rect.center
        self.rect = self.image.get_rect()
        self.rect.center = pos
    def _turn_towards(self, pos):
        angle = calc_angle(self.rect.center, pos, to_screen=False)
        diff = self.angle - angle
        if diff != 0:
            if diff > 0:
                if diff < 180:
                    adjust = -self.turn_speed
                else:
                    adjust = self.turn_speed
            else:
                if diff > -180:
                    adjust = self.turn_speed
                else:
                    adjust = -self.turn_speed
            degrees = self.angle + adjust
            degrees %= 360
            self.angle = degrees
            self._set_image()

class Mine(pygame.sprite.Sprite):
    def __init__(self, ship, angle, color='purple'):
#        jukebox['enemy fires laser'].play()
        pygame.sprite.Sprite.__init__(self)
        self.ship = ship
        self.angle = angle
        self.speed = game.speed() / 3.0
        self.image,self.rect = load_image(
            join_path('image','mine_%s.png'%color), -1)
        self.rect.center = ship.rect.center
        self.centerf = vec2d(ship.rect.center)
        self.hitmask = get_colorkey_hitmask(self.image, self.rect, -1)
        game_globals.game.groups.add(self, layer='enemy_bots')
    def hit(self, others=()):
        self.kill()
        game.groups.add(MissileExplodes(
            self.rect.center), layer='enemies', under=True)
        if len(others) == 0 and game.player.alive():
            # Bonus for dodging a live mine until end of the wave
            game.score.add(_SCORES[PATROL], 5, self.rect.center)
        else:
            # Normal score for shooting a mine
            for other in others:
                if issubclass(type(other), player.LaserBlast):
                    game.score.add(_SCORES[DERELICT], 1, self.rect.center)
                    other.kill()
    def update(self, *args):
        self.centerf.x += -sin(radians(self.angle)) * self.speed
        self.centerf.y += -cos(radians(self.angle)) * self.speed
        self.rect.centerx = iround(self.centerf.x)
        self.rect.centery = iround(self.centerf.y)
        if not game.window.rect.collidepoint(self.rect.center):
            self.kill()

class LaserBlast(pygame.sprite.Sprite):
    def __init__(self, ship, color='orange', angle=None):
        jukebox['enemy fires laser'].play()
        pygame.sprite.Sprite.__init__(self)
        self.ship = ship
        self.speed = LASER_SHOT_SPEED * game.speed()
        if angle is None:
            angle = ship.angle
        self.image,self.rect = load_image(
            join_path('image','laserblast_%s.png'%color), -1)
        self.image = pygame.transform.rotate(self.image, angle)
        self.angle = angle % 360
        self.rect = self.image.get_rect()
        self.rect.center = ship.rect.center
        self.centerf = vec2d(ship.rect.center)
        self.hitmask = get_colorkey_hitmask(self.image, self.rect, -1)
        game_globals.game.groups.add(self, layer='enemy_shots')
    def power_up(self, others):
        for other in others:
            other.hit(self)
    def update(self, *args):
        self.centerf.x += -sin(radians(self.angle)) * self.speed
        self.centerf.y += -cos(radians(self.angle)) * self.speed
        self.rect.centerx = iround(self.centerf.x)
        self.rect.centery = iround(self.centerf.y)
        if not game.window.rect.collidepoint(self.rect.center):
            self.kill()
            self.ship.shots -= 1

class MissileExplodes(pygame.sprite.Sprite):
    def __init__(self, pos):
        pygame.sprite.Sprite.__init__(self)
        jukebox['enemy explodes'].play()
        self.pos = pos
        self.images = SpriteStripAnim(join_path('image', 'Explode2x200.png'),
            (1,1,24,24), 8, -1, False, game.fps.fps/12, name='missile_explodes')
        self.images.iter()
    def update(self, *args):
        try:
            self.image = self.images.next()
        except StopIteration:
            self.kill()
        self.rect = self.image.get_rect()
        self.rect.center = self.pos

class EnemyExplodes(pygame.sprite.Sprite):
    def __init__(self, pos):
        pygame.sprite.Sprite.__init__(self)
        jukebox['enemy explodes'].play()
        self.pos = pos
        self.images = SpriteStripAnim(join_path('image', 'Explode1x200.png'),
            (0,1,48,48), 8, -1, False, game.fps.fps/12, name='enemy_explodes')
        self.images.iter()
    def update(self, *args):
        try:
            self.image = self.images.next()
        except StopIteration:
            self.kill()
        self.rect = self.image.get_rect()
        self.rect.center = self.pos

class Enemy(pygame.sprite.Sprite):
    """base class for enemy ships"""
    def __init__(self, rank, pos):
        pygame.sprite.Sprite.__init__(self)
        game = game_globals.game
        self.rect = pygame.Rect((1,1,1,1))
        self.rect.centerx,self.rect.top = pos
        self.rank = rank
        self.points = None
        self.path = None
        self.step = 0
        self.set_speed()
        self.shots = 0
        self.fire_rate = round(ENEMY_FIRE_RATE * game.speed())
        self.spec_rate = round(ENEMY_FIRE_RATE * game.speed() / 2)
        self.state = CONVOY
        self.angle = 0
        self.trail = RingBuffer(15)
        self.trail.append(*[self.rect.center]*15)
        self.countdown = game.max_fps/120
        self.angle_size = _SHIP_ANGLE_SIZE
        self.leg = 0
    def update(self, *args):
        p1 = self.rect.center
        state = self.state
        if state == STANDBY:
#            print 'state: STANDBY ...'
            self.rect.center = _STANDBY_POS[self.rank[0]][self.rank[1]]
            if self.countdown <= 0:
                self.turn_towards(0)
#                self.countdown = game.avg_fps/120
                self.countdown = game.max_fps/120
            else:
                self.countdown -= 1
        elif round(self.step) < len(self.path):
            # More on this path remains: keep moving ship
            path = self.path
            step = self.step
            rect = self.rect
            path_length = len(self.path)
            center = rect.center
            speed = self.speed
            self.trail.append(center)
            if self.state in (CONVOY,GOTO_STANDBY):
                speed *= 1.5
            while distance(center, path[iround(step)]) < speed:
#                sys.stdout.write('x')
                step += speed
                if round(step) >= path_length:
                    step = path_length - 1
                    break
#            sys.stdout.write('.')
            rect.center = self.path[int(round(step))]
            self.step = step + speed
            if state in (PATROL,CONVOY):
                self.shoot()
        elif state == CONVOY:
#            print 'state: CONVOY -> GOTO_STANDBY'
            ## Compute new control points; this ends up plotting a curve
            ## along an inverted L
            #
            # last path -> same vector -> midway between... -> standby pos
            fm = vec2d(self.points[-2])
            to = vec2d(self.points[-1])
            dx = fm.x - to.x
            dy = fm.y - to.y
            fm = vec2d(self.path[-1])
            ## fm=last path
            points = [fm]
            to = vec2d(fm.x-dx,fm.y-dy)
            ## upwards along same vector
            points.append(to)
            fm = to
            rankx,ranky = self.rank[0],self.rank[1]
            to = vec2d(_STANDBY_POS[rankx][ranky])
            dx = fm.x - to.x
            dy = fm.y - to.y
            midx = fm.x - dx/2
            midy = fm.y - dy/2
            ## left or right, midway between...
            points.append(vec2d(midx,midy))
            ## final destination: standby pos
            points.append(vec2d(to))
            self.new_path(points)
            self.state = GOTO_STANDBY
        elif state == GOTO_STANDBY:
#            print 'state: GOTO_STANDBY -> STANDBY'
            rankx,ranky = self.rank[0],self.rank[1]
            x,y = _STANDBY_POS[rankx][ranky]
            self.path = [vec2d(x, y)]
            self.step = 0
            self.state = STANDBY
#            print 'state: STANDBY...'
        elif state == RAM:
            self.state = PATROL
        elif state == PATROL:
            # sets END_PATROL when done generating patrol behaviors
            self.new_path()
        elif state == END_PATROL:
            rect = self.rect
#            print 'state: PATROL -> REFORM'
            fm = vec2d(rect.centerx,-50)
            to = vec2d(rect.centerx, 0)
            points = [vec2d(fm),vec2d(to)]
            fm = to
            rankx,ranky = self.rank[0],self.rank[1]
            to = _STANDBY_POS[rankx][ranky]
            for i in range(2,4):
                midx = fm.x + int((to.x - fm.x) * i/3)
                midy = fm.y + int((to.y - fm.y) * i/3)
                points.append(vec2d(midx,midy))
            self.new_path(points)
            game.enemy_factory._mark_lull()
            self.state = REFORM
        elif state == REFORM:
#            print 'state: REFORM -> STANDBY'
            rankx,ranky = self.rank[0],self.rank[1]
            self.rect.center = _STANDBY_POS[rankx][ranky]
#            print 'state: STANDBY...'
            game.enemy_factory._mark_lull()
            self.state = STANDBY
        if state != STANDBY:
            i = int(round(self.step+10))
            if i < len(self.path):
                p2 = self.path[i]
                self._set_image(p2)
    def new_path(self, points=None, path=None):
        if points is None:
            self.points = []
        else:
            self.points = points
        if path is None:
            self.path = plot_curve(self.points)
        else:
            self.path = path
        self.step = 0
    def turn_towards(self, angle):
        degrees = iround(angle/self.angle_size) * self.angle_size
        if degrees != self.angle:
            degrees = self.angle - angle
            degrees %= 360
            if degrees > 0 and degrees < 180:
                degrees -= self.angle_size
            elif degrees >= 180 and degrees < 360:
                degrees += self.angle_size
            degrees += angle
            degrees = iround(degrees/self.angle_size)
            degrees *= self.angle_size
            degrees %= 360
            self.angle = degrees
        self.image,self.hitmask = self.anim.get_angle(
            random.choice((0,0,0,0,0,1)), degrees)
        pos = self.rect.center
        self.rect.size = self.image.get_size()
        self.rect.center = pos
    def _set_image(self, to_point):
        from_point = self.trail.get()[0]
        degrees = calc_angle(from_point, to_point, to_screen=False)
        degrees = int(round(degrees/self.angle_size))
        degrees *= self.angle_size
        # debug for a single sprite
#        if self.rank == (2,0):
#            print degrees
        self.image,self.hitmask = self.anim.get_angle(
            random.choice((0,1,1,2,2,2,2)), degrees)
        pos = self.rect.center
        self.rect.size = self.image.get_size()
        self.rect.center = pos
        self.angle = degrees
    def set_speed(self, new_speed=None):
        if new_speed is None:
            self.speed = ENEMY_SPEED * game_globals.game.speed()
        else:
            self.speed = new_speed
    def hit(self, others):
        for other in others:
            self.kill()
            other.kill()
            if issubclass(type(other), (player.LaserBlast,player.Ship)):
                game.score.add(_SCORES[self.state], self._bonus(), self.rect.center)
                game.groups.add(EnemyExplodes(
                    self.rect.center), layer='enemies', under=True)
            if randrange(5) == 0:
                ExtraShipDebris(self.image, self.rect.center)
    def _bonus(self):
        """return bonus multiplier; override to tweak awarded points"""
        return 1
    def power_up(self, others):
        for other in others:
            other.hit(self)
    def shoot(self):
        """enemy attack logic; override and call the parent method"""
        if self.fire_rate > 0:
            self.fire_rate -= 1
        if self.spec_rate > 0:
            self.spec_rate -= 1
    def _strafing_attack(self):
        """aimless strafing attack"""
        path_len = len(self.path)
        step = int(round(self.step))
        if path_len < 10:
            x1,y1 = self.path[0]
            x2,y2 = self.path[-1]
        elif path_len > step + 9:
            x1,y1 = self.path[step]
            x2,y2 = self.path[step+9]
        else:
            x1,y1 = self.path[-1]
            x2,y2 = self.path[-10]
#            x1 += 10; y1 += 10
#            x2 += 10; y2 += 10
        if y1 < y2:
#            LaserBlast(self, (x1,y1), (x2,y2))
            LaserBlast(self)
            self.shots += 1
            self.fire_rate = round(ENEMY_FIRE_RATE * game.speed())

class Fighter(Enemy):
    def __init__(self, rank, pos):
        Enemy.__init__(self, rank, pos)
        self.anim = SpriteStripAnim(_SPACE1_SPRITE_SHEET, (11,259,36,23), 3,
            colorkey=-1, hitmasks=False, angles=range(0,361,self.angle_size),
            name='enemy_fighter')
        self.anim.load_hitmasks('enemy_fighter', 3)
    def new_path(self, points=None, path=None):
        game_left = game_rect.left
        game_right = game_rect.right
        def make_points():
            cx,cy = center = self.rect.center
            self_height = self.rect.height
            game_centerx = game_rect.centerx
            # Default path: enemy fighter flies an eccentric ? pattern
            points = [
                vec2d(center),
                vec2d(443, 0),vec2d(527, 0),vec2d(527, 175),
                vec2d(527, 266),vec2d(428, 369),vec2d(286, 452),
                vec2d(164, 521),vec2d(86, 625),vec2d(86, 767+self_height),
            ]
            # Rules to tweak runtime path.
            #
            # Enemy points start out right-facing. If enemy is left-facing,
            # swap the x values to the other side. Then introduce some
            # randomness to the pattern as follows:
            #
            # tweak 2.x near to sprite.x (subject to left/right-facedness)
            # tweak 9.x to ram player
            # replace 1.x with sprite.x (subject to left/right-facedness)
            # set 3.x and 4.x to 2.x
            # vary 5.x through 9.x to kick out ? tail
            # vary 3.y through 7.y for height of ? tail
            if cx >= game_centerx:
                # Default path already facing the right way; set 2.x, 9.x
                points[2].x = cx + randrange(40, 80)
                if randrange(5) == 0:
                    points[9].x = game.player.rect.centerx
                else:
                    points[9].x = game_left
            else:
                # Flip default path to face other way; set 2.x, 9.x
                for p in points[2:]:
                    p.x = game_centerx - (p.x - game_centerx)
                points[2].x = cx - randrange(40, 80)
                if randrange(5) == 0:
                    points[9].x = game.player.rect.centerx
                else:
                    points[9].x = game_right
            # Tweak the rest to add some unpredictability
            points[1].x = cx
            points[3].x = points[4].x = points[2].x
            randx = randrange(0,60) - 30
            for i in (5,6,7,8,9):
                points[i].x += randx
            randy = randrange(0,120) - 100
            for i in (3,4,5,6,7):
                points[i].y += randy
            return points
        if points is None:
            if self.leg == 0:
                points = make_points()
                self.leg = 1
            elif self.leg == 1:
                self.state = END_PATROL
                self.leg = 0
        # Knock x inbounds
        if points is not None:
            for p in points[1:]:
                if p.x > game_right:
                    p.x = game_right
                if p.x < game_left:
                    p.x = game_left
        Enemy.new_path(self, points, path)
    def shoot(self):
        Enemy.shoot(self)
        if self.state == PATROL:
            mx = 0.4
        else:
            mx = 1.8 #1.3
        fire_now = self.shots < ENEMY_MAX_SHOTS and \
            randrange(0,round(game.max_fps*mx)) == 0
        if fire_now:
            angle_to_player = round(calc_angle(
                self.rect.center, game.player.rect.center, to_screen=False))
            spec_now = self.shots < ENEMY_MAX_SHOTS and \
                angle_to_player in range(self.angle-5, self.angle+5)
            if spec_now and self.spec_rate <= 0:
                # Special aimed attack
                LaserBlast(self, 'green')
                self.shots += 1
                self.spec_rate = round(ENEMY_FIRE_RATE * game.speed() / 2)
            elif self.fire_rate <= 0:
                # Normal attack
                self._strafing_attack()
    def _bonus(self):
        """double default score if on patrol"""
        if self.state == PATROL:
            bonus = 2
        else:
            bonus = 1
        return bonus

class Sweeper(Enemy):
    def __init__(self, rank, pos):
        Enemy.__init__(self, rank, pos)
        self.anim = SpriteStripAnim(_SPACE2_SPRITE_SHEET, (191,105,43,35), 3,
            colorkey=-1, hitmasks=False, angles=range(0,361,self.angle_size),
            name='enemy_sweeper')
        self.anim.load_hitmasks('enemy_sweeper', 3)
    def new_path(self, points=None, path=None):
        game_left = game_rect.left
        game_right = game_rect.right
        def make_points():
            cx,cy = center = self.rect.center
            self_height = self.rect.height
            game_centerx = game_rect.centerx
            # Default path: enemy sweeper flies a wide S pattern
            points = [
                vec2d(center),
                vec2d(400, 0),vec2d(527, 0),vec2d(527, 76),
                vec2d(527, 241),vec2d(383, 241),vec2d(193, 284),
                vec2d(17, 325),vec2d(17, 446),vec2d(129, 509),
                vec2d(275, 573),vec2d(574, 573),vec2d(574, 767+self_height),
            ]
            # Rules to tweak runtime path.
            #
            # Enemy points start out right-facing. If enemy is left-facing,
            # swap the x values to the other side. Then introduce some
            # randomness to the pattern as follows:
            #
            # tweak 2.x near to sprite.x (subject to left/right-facedness)
            # tweak 12.x to ram player
            # replace 1.x with sprite.x (subject to left/right-facedness)
            # set 3.x and 4.x to 2.x
            # vary 6.x through 9.x for width of S tail
            # vary 5.y through 7.y for top of S tail
            # vary 8.y through 11.y for bottom of S tail
            if cx >= game_centerx:
                # Default path already facing the right way; set 2.x, 9.x
                points[2].x = cx + randrange(20, 60)
                if randrange(5) == 0:
                    points[12].x = game.player.rect.centerx
                else:
                    points[12].x = game_left
            else:
                # Flip default path to face other way; set 2.x, 9.x
                for p in points[2:]:
                    p.x = game_centerx - (p.x - game_centerx)
                points[2].x = cx - randrange(20, 60)
                if randrange(5) == 0:
                    points[12].x = game.player.rect.centerx
                else:
                    points[12].x = game_right
            # Tweak the rest to add some unpredictability
            points[1].x = cx
            points[3].x = points[4].x = points[2].x
            randx = randrange(0,60) - 30
            for i in (6,7,8,9):
                points[i].x += randx
            randy = randrange(0,80) - 60
            for i in (5,6,7):
                points[i].y += randy
            randy = randrange(0,80) - 40
            for i in (8,9,10,11):
                points[i].y += randy
            return points
        if points is None:
            if self.leg == 0:
                points = make_points()
                self.leg = 1
            elif self.leg == 1:
                self.state = END_PATROL
                self.leg = 0
        # Knock x inbounds
        if path is not None:
            for p in points[1:]:
                if p.x > game_right:
                    p.x = game_right
                if p.x < game_left:
                    p.x = game_left
        Enemy.new_path(self, points, path)
    def shoot(self):
        Enemy.shoot(self)
        if self.state == PATROL:
            mx = 0.4
        else:
            mx = 1.8 #1.3
        fire_now = self.shots < ENEMY_MAX_SHOTS and \
            randrange(0,round(game.max_fps*mx)) == 0
        if fire_now:
            angle_to_player = round(calc_angle(
                self.rect.center, game.player.rect.center, to_screen=False))
            spec_now = self.shots < ENEMY_MAX_SHOTS and \
                angle_to_player in range(self.angle-5, self.angle+5)
            if spec_now and self.spec_rate <= 0:
                # Special aimed attack
                LaserBlast(self, 'red')
                self.shots += 1
                self.spec_rate = round(ENEMY_FIRE_RATE * game.speed() / 2)
            elif self.fire_rate <= 0:
                # Normal attack
                self._strafing_attack()

class Kamikaze(Enemy):
    def __init__(self, rank, pos):
        Enemy.__init__(self, rank, pos)
        self.angle_size = 4
        self.anim = SpriteStripAnim(_KAMIKAZE_SPRITE_SHEET, (0,0,41,41), 3,
            colorkey=-1, hitmasks=False, angles=range(0,361,self.angle_size),
            name='enemy_kamikaze')
        self.anim.load_hitmasks('enemy_kamikaze', 3)
    def new_path(self, points=None, path=None):
        game_left = game_rect.left
        game_right = game_rect.right
        if points is None:
            cx,cy = center = self.rect.center
            self_height = self.rect.height
            game_centerx = game_rect.centerx
            if self.leg == 0:
                ## Go to park
                if cx > game_centerx:
                    points = [
                        vec2d(center), vec2d(cx,cy-100),
                        vec2d(cx+100,cy-100), vec2d(cx+100,_STANDBY_Y[-1]+25)
                    ]
                else:
                    points = [
                        vec2d(center), vec2d(cx,cy-100),
                        vec2d(cx-100,cy-100), vec2d(cx-100,_STANDBY_Y[-1]+25)
                    ]
                self.leg += 1
            elif self.leg == 1:
                ## Park
                self.pause = 120 * 2 / game.speed()
                self.leg += 1
                missile_count = 0
#                launch_total = 1 + (game.wave()+1)/10
                launch_total = int(game.enemy_factory.max_on_patrol[type(self)])
                angles = random.sample(range(0,360,20), launch_total)
                while missile_count < launch_total:
                    Missile(self, angles[missile_count])
                    missile_count += 1
            elif self.leg == 2:
                ## Vulture
                if self.pause > 0:
                    self.turn_towards(calc_angle(
                        center, game.player.rect.center, to_screen=False))
                    self.pause -= 1
                else:
                    self.leg += 1
            elif self.leg == 3:
                ## Ram
                srect = game.player.rect
                # reset trail so sprite doesn't twitch after pivoting
                trail = self.trail.get()
                trail[:] = [center]*len(trail)
                if randrange(0,2):
                    path = calc_line(center, srect.center)
                    self.set_speed(self.speed + game.speed())
                else:
                    dy = srect.centery - cy
                    if cx > game_centerx:
                        points = [
                            vec2d(center), vec2d(cx+200,cy+dy*1/3),
                            vec2d(cx+200,cy+dy*2/3), vec2d(srect.center),
                        ]
                    else:
                        points = [
                            vec2d(center), vec2d(cx-200,cy+dy*1/3),
                            vec2d(cx-200,cy+dy*2/3), vec2d(srect.center),
                        ]
                self.leg += 1
                self.state = RAM
            elif self.leg == 4:
                ## Escape
                to_rect = game.window.rect
                path = calc_line(
                    self.rect.center, (cx,to_rect.height+30))
                self.leg += 1
            elif self.leg == 5:
                ## Reform
                self.state = END_PATROL
                self.set_speed()
                self.leg = 0
        # Knock x inbounds
        if points is not None:
            for p in points[1:]:
                if p.x > game_right:
                    p.x = game_right
                if p.x < game_left:
                    p.x = game_left
        Enemy.new_path(self, points, path)
    def _bonus(self):
        """3X default score if on patrol"""
        if self.state in (PATROL,RAM):
            bonus = 3
        else:
            bonus = 1
        return bonus

class Destroyer(Enemy):
    def __init__(self, rank, pos):
        game = game_globals.game
        Enemy.__init__(self, rank, pos)
        self.anim = SpriteStripAnim(_SPACE1_SPRITE_SHEET, (196,198,76,45), 3,
            colorkey=-1, hitmasks=False, angles=range(0,361,self.angle_size),
            name='enemy_destroyer')
        self.anim.load_hitmasks('enemy_destroyer', 3)
        self.spec_reset = game.max_fps * 5
        self.spec_rate = self.spec_reset
    def new_path(self, points=None, path=None):
        game_left = game_rect.left
        game_right = game_rect.right
        def make_points():
            cx,cy = center = self.rect.center
            self_height = self.rect.height
            game_centerx = game_rect.centerx
            points = [
                vec2d(center),
                vec2d(555, 0),vec2d(682, 0),vec2d(682, 71),
                vec2d(682, 139),vec2d(614, 209),vec2d(553, 272),
                vec2d(485, 342),vec2d(427, 407),vec2d(489, 487),
                vec2d(555, 581),vec2d(617, 660),vec2d(709, 763)
            ]
            if cx < game_centerx:
                for p in points[2:]:
                    p.x = game_centerx - (p.x - game_centerx)
            return points
        if points is None:
            if self.leg == 0:
                points = make_points()
                self.leg = 1
            elif self.leg == 1:
                self.state = END_PATROL
                self.leg = 0
        # Knock x inbounds
        if points is not None:
            for p in points[1:]:
                if p.x > game_right:
                    p.x = game_right
                if p.x < game_left:
                    p.x = game_left
        Enemy.new_path(self, points, path)
    def shoot(self):
        if self.state != PATROL:
            return
        Enemy.shoot(self)
        fire_now = self.spec_rate <= 0
        if fire_now:
            angle = calc_angle(
                self.rect.center, game.player.rect.center, to_screen=False)
            # Every five waves Destroyer drops an extra mine
            mine_count = 0
            launch_total = int(game.enemy_factory.max_on_patrol[type(self)])
            variances = random.sample(range(0,20,2), launch_total)
            while mine_count < launch_total:
                variance = variances[mine_count] - 10
                Mine(self, angle+variance)
                mine_count += 1
            self.spec_rate = self.spec_reset
        else:
            self.spec_rate -= 1
    def _bonus(self):
        """4X default score if on patrol"""
        if self.state == PATROL:
            bonus = 4
        else:
            bonus = 1
        return bonus

# the y position of the ranks in the standby formation
_STANDBY_Y = [100, 150, 200, 250]
# the convoy paths that enemy sprites follow for each stage
# len(_CONVOY_PATHS) must equal len(_FLEETS)
_CONVOY_PATHS = [
    [   ## wave 1
        vec2d(134, -20),
        vec2d(204, 163),vec2d(260, 274),vec2d(393, 323),
        vec2d(555, 358),vec2d(695, 375),vec2d(711, 443),
        vec2d(721, 500),vec2d(682, 550),vec2d(613, 565),
        vec2d(536, 559),vec2d(464, 539),vec2d(389, 508),
        vec2d(325, 467),vec2d(257, 415),vec2d(229, _STANDBY_Y[-1]+100),
    ],
    [   ## wave 2
        [
            vec2d(487, 0),
            vec2d(501, 95),vec2d(526, 216),vec2d(587, 284),
            vec2d(662, 361),vec2d(719, 475),vec2d(633, 531),
            vec2d(544, 570),vec2d(504, 454),vec2d(479, _STANDBY_Y[-1]+100),
        ],
        [
            vec2d(313, 0),
            vec2d(299, 95),vec2d(274, 216),vec2d(213, 284),
            vec2d(138, 361),vec2d(81, 475),vec2d(167, 531),
            vec2d(256, 570),vec2d(296, 454), vec2d(321, _STANDBY_Y[-1]+100)
        ],
    ],
    [   ## wave 3
        vec2d(526, -20),
        vec2d(526, 91),vec2d(502, 192),vec2d(445, 284),
        vec2d(392, 357),vec2d(315, 435),vec2d(215, 518),
        vec2d(116, 578),vec2d(43, 473),vec2d(130, 412),
        vec2d(212, 352),vec2d(310, 313),vec2d(434, 313),
        vec2d(540, 315),vec2d(626, 361),vec2d(688, 409),
        vec2d(767, 475),vec2d(682, 570),vec2d(604, 495),
        vec2d(539, 433),vec2d(479, 354),vec2d(420, _STANDBY_Y[-1]+100),
    ],
    [   ## wave 4
        [
            vec2d(199, -20),
            vec2d(220, 119),vec2d(249, 248),vec2d(284, 358),
            vec2d(326, 465),vec2d(406, 581),vec2d(502, 581),
            vec2d(594, 581),vec2d(668, 523),vec2d(662, 443),
            vec2d(654, 361),vec2d(543, 404),vec2d(508, _STANDBY_Y[-1]+100),
        ],
        [
            vec2d(238, -20),
            vec2d(260, 117),vec2d(287, 239),vec2d(319, 354),
            vec2d(356, 450),vec2d(426, 542),vec2d(508, 542),
            vec2d(580, 542),vec2d(625, 506),vec2d(626, 450),
            vec2d(626, 400),vec2d(516, 439),vec2d(453, _STANDBY_Y[-1]+100),
        ],
    ],
    [   ## wave 5
        vec2d(227, -20),
        vec2d(342, 40),vec2d(475, 101),vec2d(576, 154),
        vec2d(676, 217),vec2d(780, 289),vec2d(780, 390),
        vec2d(780, 477),vec2d(670, 593),vec2d(563, 526),
        vec2d(468, 462),vec2d(533, 331),vec2d(627, 401),
        vec2d(724, 511),vec2d(503, 579),vec2d(411, 525),
        vec2d(305, 460),vec2d(386, 331),vec2d(475, 390),
        vec2d(579, 499),vec2d(355, 560),vec2d(253, 505),
        vec2d(142, 438),vec2d(214, 316),vec2d(315, 375),
        vec2d(418, 482),vec2d(201, 539),vec2d(127, 485),
        vec2d(68, 440),vec2d(121, 400),vec2d(174, _STANDBY_Y[-1]+100),
    ],
    [   ## wave 6 (mirror wave 4)
        [
            vec2d(638, -20),
            vec2d(580, 119),vec2d(551, 248),vec2d(516, 358),
            vec2d(474, 465),vec2d(394, 581),vec2d(298, 581),
            vec2d(206, 581),vec2d(132, 523),vec2d(138, 443),
            vec2d(146, 361),vec2d(257, 404),vec2d(292, _STANDBY_Y[-1]+100),
        ],
        [
            vec2d(599, -20),
            vec2d(540, 117),vec2d(513, 239),vec2d(481, 354),
            vec2d(444, 450),vec2d(374, 542),vec2d(292, 542),
            vec2d(220, 542),vec2d(175, 506),vec2d(174, 450),
            vec2d(174, 400),vec2d(284, 439),vec2d(347, _STANDBY_Y[-1]+100),
        ],
    ],
    [   ## wave 7
        vec2d(150, -20),
        vec2d(245, 24),vec2d(464, 24),vec2d(358, 94),
        vec2d(253, 176),vec2d(417, 219),vec2d(386, 283),
        vec2d(334, 392),vec2d(259, 467),vec2d(116, 567),
        vec2d(273, 487),vec2d(501, 420),vec2d(705, 409),
        vec2d(445, 341),vec2d(307, 341),vec2d(71, 412),
        vec2d(254, 419),vec2d(471, 490),vec2d(640, 569),
        vec2d(487, 477),vec2d(410, 393),vec2d(357, _STANDBY_Y[-1]+100),
    ],
]
# _UNIT_TYPES decodes _FLEETS into classes
_UNIT_TYPES = {
    'f' : Fighter,
    's' : Sweeper,
    'k' : Kamikaze,
    'd' : Destroyer,
}
_MAX_ON_PATROL = {
    Fighter : 2,
    Sweeper : 2,
    Kamikaze: 1,
    Destroyer : 1,
}
_N_ON_PATROL = {
    Fighter : [],
    Sweeper : [],
    Kamikaze: [],
    Destroyer  : [],
}
_N_ON_STANDBY = {
    Fighter : [],
    Sweeper : [],
    Kamikaze: [],
    Destroyer : [],
}
# the standby formation and creation order (bottom-left to top-right) of
# enemies for each stage layout
_FLEETS = (
    [   ## wave 1
        '',
        '',
        'ffssssff',
        'ffffffff',
    ],
    [   ## wave 2
        '',
        '  fssf  ',
        'ffssssff',
        'ffffffff',
    ],
    [   ## wave 3
        '  k  k  ',
        '  fssf  ',
        'ffssssff',
        'ffffffff',
    ],
    [   ## wave 4
        '  d  d  ',
        '  fssf  ',
        'ssffffss',
        'ffffffff',
    ],
    [   ## wave 5
        ' kk  kk ',
        'sssffsss',
        'ssffffss',
        'ffffffff',
    ],
    [   ## wave 6
        ' dd  dd ',
        'sffffffs',
        'ssffffss',
        'ssssssss',
    ],
    [   ## wave 7
        'fsfsfsfs',
        'sfsfsfsf',
        'fsfsfsfs',
        'ffsffsff',
    ],
    [   ## wave 8
        ' ddkkdd ',
        'sffffffs',
        'sfsfsfsf',
        'ssfssfss',
    ],
    [   ## wave 9
        ' kkddkk ',
        'ffssssff',
        'ssffffss',
        'ffssssff',
    ],
    [   ## wave 10
        'ddddkkkk',
        'ssssffff',
        'ssssffff',
        'ssssffff',
    ],
)
# the x,y position of all enemy sprites while in standby formation
_STANDBY_POS = [
    [],
    [],
    [],
    [],
]

class StandbyStepper(object):
    """perpetual iterator that generates wavy movement for sprites in standby"""
    def __init__(self, lzeroes, ones, zeroes, start=0):
        self.size = x = game.speed()
        self.steps = [0]*lzeroes
        for i in range(4):
            self.steps.extend([x]*ones)
            self.steps.extend([0]*zeroes)
        self.steps.extend([x]*ones)
        self.steps.extend([0]*(zeroes-lzeroes))
        t = [x * -1 for x in self.steps]
        self.steps.extend(t)
        self.width = len(self.steps)
        self.start = self.i = start
        self.throttle = 6 / self.size
        self.countdown = self.throttle
        self.wave_length = ones + zeroes
        self.pause = 0
    def iter(self):
        self.i = 0
        return self
    def next(self):
        if self.pause > 0:
            self.pause -= 1
            val = 0
        else:
            if self.i >= self.width:
                self.iter()
            if self.countdown <= 0:
                val = self.steps[self.i]
                self.countdown = self.throttle
                self.i += 1
                if self.i % self.wave_length == 0:
                    self.pause = 120 / game.speed()
            else:
                val = 0
                self.countdown -= 1
        return val

class ConvoyPath(object):
    """serve spline points as a convenience for Factory loops"""
    def __init__(self, stage):
        self.stage = stage % len(_CONVOY_PATHS)
        self.points = _CONVOY_PATHS[self.stage]
        if type(self.points[0]) is list:
            # multi-path convoy
            self.multi = True
            self.rlen = len(self.points)
            self.range = range(self.rlen)
            self._count = len(self.points[0])
            self.path = []
            for i in range(self.rlen):
                self.path.append(plot_curve(self.points[i]))
        else:
            # single-path convoy
            self.multi = False
            self.rlen = 1
            self.range = (0,)
            self.count = len(self.points)
            self.path = plot_curve(self.points)
        self.r = 0
    def num_paths(self):
        return self.rlen
    def len_path(self):
        return self.count
    def iter(self):
        self.r = 0
        return self
    def next(self):
        """return tuple (points, path)"""
        if self.r >= self.rlen:
            self.r = 0
        if self.multi:
            val = (self.points[self.r], self.path[self.r])
        else:
            val = (self.points, self.path)
        self.r += 1
        return val

class Factory(object):
    """manufacture enemies for incoming wave"""
    def __init__(self):
        global game, window, game_rect, jukebox
        game = game_globals.game
        window = game.window
        game_rect = window.rect
        jukebox = gumm_sound.jukebox
        jukebox.load_sound(join_path('sound','laser_26667e.ogg'),
            'enemy fires laser', volume=0.2, sound_type=SOUND_FX)
        jukebox.load_ascii_file(join_path('sound','enemy_explodes.dat'),
            'enemy explodes', volume=1, sound_type=SOUND_FX)
        self.stage = game.wave() % len(_FLEETS)
        self.fleet_size = 0
        self.fleet = []
        self.convoy_path = ConvoyPath(self.stage)
        self._make_fleet()
        self.unit = 0
        self.done = False
        self.release_rate = 40 / game.speed()
        self.groups = game.groups
        self.n_patrol = 0
        self.n_standby = 0
        ## Every 10 levels add an extra baddy on patrol
        self.max_on_patrol = {}
        for k,v in _MAX_ON_PATROL.items():
            self.max_on_patrol[k] = v + (game.wave()+1.0)*0.05
        ## Timer: Every few seconds bump a random baddy's max-on-patrol
        self.more_on_patrol = game.max_fps * 2 * len(_MAX_ON_PATROL)/3
        ## Max and timer to prevent excessive lulls
        self.max_lull = 5000
        self.time = 0
        ## Timer to nudge standby formation
        self.standby_steppers = [
            StandbyStepper(10, 10, 10, 20),
            StandbyStepper(8, 10, 10, 20),
            StandbyStepper(4, 10, 10, 20),
            StandbyStepper(0, 10, 10, 20),
        ]
        # debug
        self._draw_path_key = 0
    def update(self, standby_only=False):
        if self.unit == self.fleet_size:
            if not self.is_done():
                # Enemies filing in, wait for them to be on standby
                done = True
                for s in self.groups.enemies.sprites():
                    if s.state != STANDBY:
                        done = False
                        break
                self.done = done
                self.time = pygame.time.get_ticks()
        elif self.release_rate <= 0:
            # Release an enemy for each path
            for i in range(self.convoy_path.num_paths()):
                s = self.fleet[self.unit]
                s.update()
                self.groups.enemies.add(s)
                self.groups.all.add(s, layer='enemies')
                self.unit += 1
                self.release_rate = 40 / game.speed()
        else:
            self.release_rate -= 1
        if not standby_only:
            # If player is dragging ass, put more in the air!
            if self.more_on_patrol <= 0:
                ## Every few seconds bump a random baddy's max-on-patrol
                k = random.choice(self.max_on_patrol.keys())
                self.max_on_patrol[k] += 0.1
                self.more_on_patrol = game.max_fps * 2 * len(_MAX_ON_PATROL)/3
            else:
                self.more_on_patrol -= 1
            self._send_patrol()
        self._step_standby()
    def _mark_lull(self):
        self.time = pygame.time.get_ticks()
    def _send_patrol(self):
        if not self.is_done():
            return
        patrol = _N_ON_PATROL
        standby = _N_ON_STANDBY
        for k in patrol.keys():
            del patrol[k][:]
            del standby[k][:]
        self.n_patrol = 0
        self.n_standby = 0
        for s in self.groups.enemies.sprites():
            if s.state == PATROL:
                patrol[type(s)].append(s)
                self.n_patrol += 1
            elif s.state == STANDBY:
                standby[type(s)].append(s)
                self.n_standby += 1
        for t in _MAX_ON_PATROL.keys():
            if pygame.time.get_ticks() - self.max_lull >= self.time:
                send_now = True
            else:
                send_now = randrange(0, round(game.max_fps*2)) == 0
            if send_now and len(patrol[t]) < int(self.max_on_patrol[t]):
                send_n = int(self.max_on_patrol[t]) - len(patrol[t])
                if send_n > 1:
                    send_n = randrange(1,send_n)
                self._send_adjacent(send_n, standby[t])
    def _send_adjacent(self, n, choices):
        if n > 0 and len(choices) > 0:
            which = random.choice(choices)
            which.state = PATROL
            which.new_path()
            choices.remove(which)
            x,y = which.rank
            self._mark_lull()
        n -= 1
        if n > 0 and len(choices) > 0:
            which = None
            which_dist = 3
            for s in choices:
                if which is None:
                    which = s
                    which_dist = which.rank[0]+which.rank[1]
                else:
                    rankx,ranky = s.rank
                    dx = max(x,rankx) - min(x,rankx)
                    dy = max(y,ranky) - min(y,ranky)
                    dist = dx + dy
                    if dist < which_dist:
                        which = s
                        which_dist = dist
            if which is not None:
                which.state = PATROL
                which.new_path()
                choices.remove(which)
    def _step_standby(self):
        if not self.is_done():
            return
        for i in range(len(_STANDBY_POS)):
            pos_row = _STANDBY_POS[i]
            step = self.standby_steppers[i].next()
            if step != 0:
                for p in pos_row:
                    p.x += step
    def is_done(self):
        return self.done
    def _make_fleet(self):
        window = game.window
        fleet_layout = _FLEETS[self.stage]
        screen_margin = 60
        screen_width = game_rect.width - (screen_margin*2)
        for row_num in range(len(fleet_layout)-1, 0-1, -1):
            row = fleet_layout[row_num]
            row_len = len(row)
            _STANDBY_POS[row_num] = []
            y = _STANDBY_Y[row_num]
            for unit in range(0,row_len):
                points,path = self.convoy_path.next()
                x = (screen_width * unit/row_len) + screen_margin
                _STANDBY_POS[row_num].append(vec2d(x,y))
                if row[unit] is ' ':
                    continue
                UnitClass = _UNIT_TYPES[row[unit]]
                id = (row_num,unit)
                pos = points[0]
                enemy = UnitClass(id, pos)
                enemy.state = CONVOY
                enemy.new_path(points, path)
                self.fleet.append(enemy)
        self.fleet_size = len(self.fleet)
    def _toggle_draw_path(self, key):
        if self._draw_path_key == key:
            self._draw_path_key = 0
        else:
            self._draw_path_key = key
    def draw_path(self):
        if self._draw_path_key == 0:
            return
        try:
            surf = window.surface
            group = game.groups.enemies
            draw_lines = pygame.draw.lines
            i = self._draw_path_key - K_0
            s = group.sprites()[i]
            if len(s.path) > 1:
                blue,grey,orange = Color('blue'),Color('grey'),Color('orange')
                draw_lines(surf, grey, False, s.points)
                for x,y in s.points:
                    pygame.draw.circle(surf, blue, (int(x),int(y)), 3)
                draw_lines(surf, orange, False, s.path)
        except IndexError:
            pass

if __name__ == '__main__':
    ## test: cycle sprite strips to check for jitters
    if len(sys.argv) > 1:
        which = sys.argv[1]
    else:
        which = 'fighter'
    os.chdir('..')
    from pygame.locals import Color, KEYUP, K_ESCAPE
    pygame.init()
    clock = pygame.time.Clock()
    surf = pygame.display.set_mode((200,200))
    bg = surf.copy()
    bg.convert()
    bg.fill(Color('black'))
    if which == 'fighter':
        anim = SpriteStripAnim(_SPACE1_SPRITE_SHEET, (11,259,36,23), 3,
            colorkey=-1, hitmasks=True, angles=range(0,361,_SHIP_ANGLE_SIZE),
            name='enemy_fighter')
    elif which == 'sweeper':
        anim = SpriteStripAnim(_SPACE2_SPRITE_SHEET, (191,105,43,35), 3,
            colorkey=-1, hitmasks=True, angles=range(0,361,_SHIP_ANGLE_SIZE),
            name='enemy_sweeper')
    elif which == 'kamikaze':
        anim = SpriteStripAnim(_KAMIKAZE_SPRITE_SHEET, (0,0,41,41), 3,
            colorkey=-1, hitmasks=True, angles=range(0,361,_SHIP_ANGLE_SIZE),
            name='enemy_kamikaze')
    elif which == 'destroyer':
        anim = SpriteStripAnim(_SPACE1_SPRITE_SHEET, (196,198,76,45), 3,
            colorkey=-1, hitmasks=True, angles=range(0,361,_SHIP_ANGLE_SIZE),
            name='enemy_destroyer')
    else:
        print 'usage: enemy.py fighter|sweeper|kamikaze|destroyer'
        quit()
    i = 0
    while 1:
        for e in pygame.event.get():
            if e.type == KEYUP and e.key == K_ESCAPE:
                quit()
        if i == 3:
            i = 0
        surf.blit(bg, bg.get_rect())
        image,hitmask = anim.get_angle(i, 0)
        surf.blit(image, image.get_rect())
        pygame.display.flip()
        clock.tick(2)
        i += 1
